参考链接:
https://www.lhyerror404.cn/2019/12/19/fini_array%e6%ae%b5%e5%8a%ab%e6%8c%81/
https://r0co.top/passages/%E5%88%A9%E7%94%A8LD-PRELOAD-HOOK%E7%B3%BB%E7%BB%9F%E5%86%85%E7%BD%AE%E5%87%BD%E6%95%B0/
https://www.freebuf.com/articles/web/192052.html
https://blog.csdn.net/chen_jianjian/article/details/80627693
前言
大多数可执行文件是通过链接 libc 来进行编译的,因此 gcc 会将 glibc 初始化代码放入编译好的可执行文件和共享库中。 .init_array和 .fini_array 节(早期版本被称为 .ctors和 .dtors )中存放了指向初始化代码和终止代码的函数指针。 .init_array 函数指针会在 main() 函数调用之前触发。这就意味着,可以通过重写某个指向正确地址的指针来将控制流指向病毒或者寄生代码。 .fini_array 函数指针在 main() 函数执行完之后才被触发,在某些场景下这一点会非常有用。例如,特定的堆溢出漏洞(如曾经的 Once upon a free())会允许攻击者在任意位置写4个字节,攻击者通常会使用一个指向 shellcode 地址的函数指针来重写.fini_array 函数指针。对于大多数病毒或者恶意软件作者来说, .init_array 函数指针是最常被攻击的目标,因为它通常可以使得寄生代码在程序的其他部分执行之前就能够先运行。
Demo 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <stdio.h> #include <stdlib.h>
static void start(void) __attribute__ ((constructor)); static void stop(void) __attribute__ ((destructor));
int main(int argc, char *argv[]) { printf("start == %p\n", start); printf("stop == %p\n", stop); return 0; }
void start(void) { printf("hello world!\n"); }
void stop(void) { printf("goodbye world!\n"); }
|
gcc为函数提供了几种类型的属性,其中两个是我们特别感兴趣的:构造函数(constructors)和析构函数(destructors)。程序员应当使用类似下面的方式来指定这些属性:
1 2
| static void start(void) __attribute__ ((constructor)); static void stop(void) __attribute__ ((destructor));
|
带有”构造函数”属性的函数将在main()函数之前被执行,而声明为”析构函数”属性的函数则将在after main()退出时执行。
程序运行结果如下:
1 2 3 4 5 6 7 8
| ➜ fini_array gcc test.c -o test ➜ fini_array ls test test.c ➜ fini_array ./test hello world! start == 0x4005a4 stop == 0x4005b5 goodbye world!
|
下载我们试试 objdump -h ./test
1 2 3 4 5
| ➜ fini_array objdump -h ./test 18 .init_array 00000010 0000000000600e00 0000000000600e00 00000e00 2**3 CONTENTS, ALLOC, LOAD, DATA 19 .fini_array 00000010 0000000000600e10 0000000000600e10 00000e10 2**3 CONTENTS, ALLOC, LOAD, DATA
|
可以看到.init_array的地址为 0x600e00 , .fini_array的地址为 0x600e10
在gdb中分别对这两个地址跟踪一下
1 2 3
| pwndbg> x/4xg 0x600e00 0x600e00: 0x0000000000400540 0x00000000004005a4 0x600e10: 0x0000000000400520 0x00000000004005b5
|
分析一下结果
.init_array存的 0x400540是 frame_dummy函数地址(ida里面可查看) 0x4005a4很明显是自己定义的start函数的地址
.fini_array存的 0x400540是 __do_global_dtors_aux函数地址(ida里面可查看) 0x4005b5 很明显是定义的stop函数的地址
Demo 2
我们再看一个例子,其实就是前面的test程序函数少了属性,我把它定义成静态函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <stdio.h> #include <stdlib.h>
static void start(void); static void stop(void);
int main(int argc, char *argv[]) { printf("start == %p\n", start); printf("stop == %p\n", stop);
return 0; }
void start(void) { printf("hello world!\n"); }
void stop(void) { printf("goodbye world!\n"); }
|
同样编译和运行:
1 2 3 4
| ➜ fini_array gcc test2.c -o test ➜ fini_array ./test start == 0x4005a4 stop == 0x4005b5
|
函数地址并没有变化,但是因为start/stop函数未设定析构与构造属性,所以没有在开始和结束时被调用。
我们试试 objdump -h ./test2
1 2 3 4 5
| ➜ fini_array objdump -h ./test2 18 .init_array 00000008 0000000000600e10 0000000000600e10 00000e10 2**3 CONTENTS, ALLOC, LOAD, DATA 19 .fini_array 00000008 0000000000600e18 0000000000600e18 00000e18 2**3 CONTENTS, ALLOC, LOAD, DATA
|
可以看到.init_array的地址为 0x600e10 , .fini_array的地址为 0x600e18,和test程序有点偏差。
现在我用gdb跟踪一波,查看一下.fini_array
1 2
| pwndbg> x/2xg 0x600e18 0x600e18: 0x0000000000400530 0x0000000000000000
|
明显0x0000000000400530后面的函数指针没有被填充 是0x0000000000000000,所以程序结束后不会执行stop函数
现在我们控制程序执行流程,怎么控制呢?我把.fini_array的函数指针0x0000000000400530覆盖成stop函数的地址
1 2 3
| pwndbg> set {int}0x600e18=0x4005b5 pwndbg> x/2xg 0x600e18 0x600e18: 0x00000000004005b5 0x0000000000000000
|
输入c继续执行程序
1 2 3 4 5 6
| pwndbg> c Continuing. start == 0x4005a4 stop == 0x4005b5 goodbye world! [Inferior 1 (process 7442) exited normally]
|
成功执行了stop函数,如果stop函数是一段onegadget或shellcode我们就可以直接拿下shell
分析与总结
我们来关心一下,上面的stop在什么地方被调用。
栈回溯跟踪看一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| ► 0x4005b5 <stop> push rbp 0x4005b6 <stop+1> mov rbp, rsp 0x4005b9 <stop+4> mov edi, 0x40067a 0x4005be <stop+9> call puts@plt <0x400430> 0x4005c3 <stop+14> nop 0x4005c4 <stop+15> pop rbp 0x4005c5 <stop+16> ret 0x4005c6 nop word ptr cs:[rax + rax] 0x4005d0 <__libc_csu_init> push r15 0x4005d2 <__libc_csu_init+2> push r14 0x4005d4 <__libc_csu_init+4> mov r15d, edi ───────────────────────────────────────[ STACK ]─────────────────────────────────────── 00:0000│ rsp 0x7fffffffdc68 —▸ 0x7ffff7de7df7 (_dl_fini+823) ◂— test r13d, r13d 01:0008│ r14 0x7fffffffdc70 —▸ 0x7ffff7ffe168 ◂— 0x0 02:0010│ 0x7fffffffdc78 —▸ 0x7ffff7ffe700 —▸ 0x7ffff7ffa000 ◂— jg 0x7ffff7ffa047 03:0018│ 0x7fffffffdc80 —▸ 0x7ffff7fb5000 —▸ 0x7ffff7a0d000 ◂— jg 0x7ffff7a0d047 04:0020│ r10 0x7fffffffdc88 —▸ 0x7ffff7ffd9d8 (_rtld_global+2456) —▸ 0x7ffff7dd7000 ◂— jg 0x7ffff7dd7047 05:0028│ 0x7fffffffdc90 —▸ 0x7fffffffdd60 —▸ 0x7fffffffde50 ◂— 0x1 06:0030│ 0x7fffffffdc98 —▸ 0x7ffff7de7b44 (_dl_fini+132) ◂— mov ecx, dword ptr [r12] 07:0038│ 0x7fffffffdca0 —▸ 0x7fffffffdc70 —▸ 0x7ffff7ffe168 ◂— 0x0 ─────────────────────────────────────[ BACKTRACE ]───────────────────────────────────── ► f 0 4005b5 stop f 1 7ffff7de7df7 _dl_fini+823 f 2 7ffff7a46ff8 __run_exit_handlers+232 f 3 7ffff7a47045 f 4 7ffff7a2d837 __libc_start_main+247 Breakpoint *0x4005b5 Breakpoint *0x4005B5
|
看到返回地址在_dl_fini+823,所以可以得出结论,.fini_array区节的第一个函数指针在程序结束时,由_dl_fini函数调用,所以我们可加以利用。在未开启PIE的情况下,只需实现一个任意地址写,将.fini_array区节的第一个函数指针改写成后门地址或者one_gadgets,在程序结束时便能控制流程